/*
 * NullableCheckBox.java 7 nov. 2008
 *
 * Sweet Home 3D, Copyright (c) 2008 Emmanuel PUYBARET / eTeks <info@eteks.com>
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 */
package com.eteks.sweethome3d.swing;

import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.GridLayout;
import java.awt.event.ItemEvent;
import java.awt.event.ItemListener;
import java.util.ArrayList;
import java.util.List;

import javax.swing.JCheckBox;
import javax.swing.JComponent;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;

/**
 * A check box that accepts <code>null</code> values. Thus this check box is able to
 * display 3 states : <code>null</code>, <code>false</code> and <code>true</code>.
 * @author Emmanuel Puybaret
 */
public class NullableCheckBox extends JComponent
{
	/** 
	 * Identifies a change in the check box text. 
	 */
	public static final String TEXT_CHANGED_PROPERTY = "text";
	/** 
	 * Identifies a change in the check box mnemonic. 
	 */
	public static final String MNEMONIC_CHANGED_PROPERTY = "mnemonic";
	
	private JCheckBox checkBox;
	private Boolean value = Boolean.FALSE;
	private boolean nullable;
	private List<ChangeListener> changeListeners = new ArrayList<ChangeListener>(1);
	
	/**
	 * Creates a nullable check box.
	 */
	public NullableCheckBox(String text)
	{
		// Measure check box size alone without its text
		final Dimension checkBoxSize = new JCheckBox().getPreferredSize();
		// Create a check box that displays a dash upon default check box for a null value
		this.checkBox = new JCheckBox(text)
		{
			@Override
			protected void paintComponent(Graphics g)
			{
				super.paintComponent(g);
				if (value == null)
				{
					g.drawRect(checkBoxSize.width / 2 - 3, checkBoxSize.height / 2, 6, 1);
				}
			}
		};
		// Add an item listener to change default checking logic 
		ItemListener checkBoxListener = new ItemListener()
		{
			public void itemStateChanged(ItemEvent ev)
			{
				ev.getItemSelectable().removeItemListener(this);
				// If this check box is nullable
				if (nullable)
				{
					// Checking sequence will be null, true, false
					if (getValue() == Boolean.FALSE)
					{
						setValue(null);
					}
					else if (getValue() == null)
					{
						setValue(Boolean.TRUE);
					}
					else
					{
						setValue(Boolean.FALSE);
					}
				}
				else
				{
					setValue(checkBox.isSelected());
				}
				ev.getItemSelectable().addItemListener(this);
			}
		};
		this.checkBox.addItemListener(checkBoxListener);
		
		// Add the check box and its label to this component
		setLayout(new GridLayout());
		add(this.checkBox);
	}
	
	/**
	 * Returns <code>null</code>, <code>Boolean.TRUE</code> or <code>Boolean.FALSE</code>.
	 */
	public Boolean getValue()
	{
		return this.value;
	}
	
	/**
	 * Sets displayed value in check box. 
	 * @param value <code>null</code>, <code>Boolean.TRUE</code> or <code>Boolean.FALSE</code>
	 */
	public void setValue(Boolean value)
	{
		this.value = value;
		if (value != null)
		{
			this.checkBox.setSelected(value);
		}
		else if (isNullable())
		{
			// Unselect check box to display a dash in its middle
			this.checkBox.setSelected(false);
			this.checkBox.repaint();
		}
		else
		{
			throw new IllegalArgumentException("Check box isn't nullable");
		}
		fireStateChanged();
	}
	
	/**
	 * Returns <code>true</code> if this check box is nullable.
	 */
	public boolean isNullable()
	{
		return this.nullable;
	}
	
	/**
	 * Sets whether this check box is nullable.
	 */
	public void setNullable(boolean nullable)
	{
		this.nullable = nullable;
		if (!nullable && getValue() == null)
		{
			setValue(Boolean.FALSE);
		}
	}
	
	/**
	 * Sets the mnemonic of this component.
	 * @param mnemonic a <code>VK_...</code> code defined in <code>java.awt.event.KeyEvent</code>. 
	 */
	public void setMnemonic(int mnemonic)
	{
		int oldMnemonic = this.checkBox.getMnemonic();
		if (oldMnemonic != mnemonic)
		{
			this.checkBox.setMnemonic(mnemonic);
			firePropertyChange(MNEMONIC_CHANGED_PROPERTY, oldMnemonic, mnemonic);
		}
	}
	
	/**
	 * Returns the mnemonic of this component.
	 */
	public int getMnemonic()
	{
		return this.checkBox.getMnemonic();
	}
	
	/**
	 * Sets the text of this component.
	 * @param text a <code>VK_...</code> code defined in <code>java.awt.event.KeyEvent</code>. 
	 */
	public void setText(String text)
	{
		String oldText = this.checkBox.getText();
		if (oldText != text)
		{
			this.checkBox.setText(text);
			firePropertyChange(TEXT_CHANGED_PROPERTY, oldText, text);
		}
	}
	
	/**
	 * Returns the text of this component.
	 */
	public String getText()
	{
		return this.checkBox.getText();
	}
	
	/**
	 * Sets the tool tip text displayed by this check box.
	 */
	public void setToolTipText(String text)
	{
		this.checkBox.setToolTipText(text);
	}
	
	@Override
	public void setEnabled(boolean enabled)
	{
		if (this.checkBox.isEnabled() != enabled)
		{
			this.checkBox.setEnabled(enabled);
			firePropertyChange("enabled", !enabled, enabled);
		}
	}
	
	@Override
	public boolean isEnabled()
	{
		return this.checkBox.isEnabled();
	}
	
	/**
	 * Adds a listener to this component.
	 */
	public void addChangeListener(final ChangeListener l)
	{
		this.changeListeners.add(l);
	}
	
	/**
	 * Removes a listener from this component.
	 */
	public void removeChangeListener(final ChangeListener l)
	{
		this.changeListeners.remove(l);
	}
	
	/**
	 * Fires a state changed event to listeners.
	 */
	private void fireStateChanged()
	{
		if (!this.changeListeners.isEmpty())
		{
			ChangeEvent changeEvent = new ChangeEvent(this);
			// Work on a copy of changeListeners to ensure a listener 
			// can modify safely listeners list
			ChangeListener[] listeners = this.changeListeners.toArray(new ChangeListener[this.changeListeners.size()]);
			for (ChangeListener listener : listeners)
			{
				listener.stateChanged(changeEvent);
			}
		}
	}
}
